﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;

namespace PKTools.Database
{
    class ObjectMapInfo
    {
        public readonly PropertyInfo
            PI;
        public readonly bool
            IsPK;

        public ObjectMapInfo(PropertyInfo pi)
        {
            if (Type.GetTypeCode(Sql.GetFinalType(pi.PropertyType)) == TypeCode.Object)
                throw new Exception("simple type expected, property = " + pi.Name);

            this.PI = pi;
            this.IsPK = this.PI.GetCustomAttribute<PK>() != null;
        }
    }

    public abstract partial class Sql : IDisposable
    {
        static readonly string
            defaultConnection;
        static readonly LazyCache<string, Dictionary<string, ObjectMapInfo>> 
            objectMaps;
        static readonly ThreadLocal<Sql> 
            context;

        protected readonly string
            connection;
        protected DbCommand
            command;
        readonly Dictionary<string, string> 
            _listParameters;
        readonly HashSet<Spyed> 
            _toResetElements;
        readonly bool
            openTransaction;

        static Sql()
        {
#if CLR
            defaultConnection = "context connection=true";
#else
            defaultConnection = ConfigurationSettings.AppSettings["PKTools.Connection"];
#endif
            objectMaps = new LazyCache<string, Dictionary<string, ObjectMapInfo>>(
                key => 
                    Type.GetType(key, true).
                    GetProperties().
                    Where(e => e.GetSetMethod(false) != null && e.GetGetMethod(false) != null && e.GetCustomAttribute<Skip>() == null).
                    ToDictionary(
                        e => e.GetCustomAttribute<Column>().IsNull(f => f.Key, e.Name), 
                        e => new ObjectMapInfo(e)));

            context = new ThreadLocal<Sql>();
        }

        public static Sql Context
        {
            get
            {
                if (context.Value == null)
                    throw new Exception("no sql context");

                return context.Value;
            }
        }

        public Sql(bool openTransaction, string connection)
        {
            this.connection = connection ?? defaultConnection;
            if (String.IsNullOrEmpty(this.connection))
                throw new Exception("connection not specified");

            this.openTransaction = openTransaction;

            this._listParameters = new Dictionary<string, string>();
            this._toResetElements = new HashSet<Spyed>();

            if (context.Value != null)
                throw new Exception("sql context already open");

            context.Value = this;
        }

        public static Type GetFinalType(Type type)
        {
            return Nullable.GetUnderlyingType(type) ?? type;
        }

        public void Clear()
        {
            this.command.Parameters.Clear();
            this._listParameters.Clear();
        }

        public abstract DbParameter AddInput(string key, object value, Type type = null);

        public abstract DbParameter AddOutput(string key, Type type = null);

        public void AddListInput<T>(string key, params T[] values)
        {
            if (values.Length == 0)
                throw new Exception("no inputs");

            AddListInput(key, values, default(T));
        }

        public void AddListInput<T>(string key, IEnumerable<T> values, T defaultValue)
        {
            int
                i = 0;
            var
                keys = new List<string>();
            var
                type = typeof(T);

            foreach (var v in values)
            {
                string
                    temp = key + (i++);

                AddInput(temp, v, type);
                keys.Add('@' + temp);
            }

            if (i == 0)
            {
                AddInput(key, defaultValue, type);
                return;
            }

            this._listParameters.Add('@' + key, String.Join(",", keys));
        }

        protected void Open()
        {
            if (this.command.Connection.State.In(ConnectionState.Closed, ConnectionState.Broken))
            {
                using (new Stoper("connection open"))
                    this.command.Connection.Open();

                if (this.openTransaction && this.Transaction == null)
                    this.BeginTransaction();
            }
        }

        public DbTransaction Transaction
        {
            get { return this.command.Transaction; }
        }

        public void BeginTransaction()
        {
            Open();

            if (this.Transaction != null)
                throw new Exception("transaction already started");

            this.command.Transaction = this.command.Connection.BeginTransaction();
            Log.Entry(LogSeverity.Debug, "transaction started");
        }

        public void Commit()
        {
            if (this.Transaction == null)
                throw new Exception("no transaction to commit");

            this.Transaction.Commit();  // this will null .Transaction  

            foreach (var e in this._toResetElements)
                e.__Reset();

            this._toResetElements.Clear();

            Log.Entry(LogSeverity.Debug, "transaction commit");
        }

        public void Rollback(bool requireTransaction = true)
        {
            if (this.Transaction == null)
            {
                if (requireTransaction)
                    throw new Exception("no transaction to commit");
            }
            else
                this.Transaction.Rollback();

            Log.Entry(LogSeverity.Debug, "transaction rollback");
        }

        public void Dispose()
        {
            if (this.Transaction != null)
            {
                this.Rollback();
#if DEBUG
                throw new Exception("forget to Commit?");
#endif
            }

            this.command.Connection.Close();

            context.Value = null;
        }

        protected virtual void SetCommandText(string query, object[] objs, CommandType type = CommandType.Text)
        {
            this.command.CommandType = type;

            query = String.Format(query, objs);

            foreach (var e in this._listParameters)
            {
                // replace full words only
                int
                    i = 0;

                while ((i = query.IndexOf(e.Key, i, StringComparison.OrdinalIgnoreCase)) != -1)
                {
                    int
                        n = i + e.Key.Length;

                    if (n == query.Length || !(Char.IsLetterOrDigit(query[n]) || query[n] == '_'))
                    {
                        query = query.Remove(i, e.Key.Length).Insert(i, e.Value);
                        i += e.Value.Length;
                    }
                    else
                        i += e.Key.Length;
                }
            }

            this.command.CommandText = query;
        }

        object ToNull(object value)
        {
            return value == DBNull.Value ? null : value;
        }

        public virtual decimal GetIdentity()
        {
            throw new NotSupportedException("not supported for this db.");
        }

        public virtual string FormatColumn(string column)
        {
            return column;
        }
    }
}
